﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;

namespace ClearingGraphAutomaton.Base
{
    /// <summary>
    /// Base cass representing the coordinate system.
    /// </summary>
    class Coordinates
    {
        /// <summary>
        /// Creates the instance of the coordinate system.
        /// </summary>
        public Coordinates()
        {
            this.coordinatePen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
            this.selectionPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
        }

        /// <summary>
        /// Number of rows.
        /// </summary>
        public int Rows
        {
            get { return this.rowCount; }
            set { this.rowCount = Math.Max(2, value); }
        }

        /// <summary>
        /// Number of columns.
        /// </summary>
        public int Columns
        {
            get { return this.colCount; }
            set { this.colCount = Math.Max(2, value); }
        }

        /// <summary>
        /// Draws the coordinate system to the specified area.
        /// </summary>
        /// <param name="dc">Graphical device context.</param>
        /// <param name="rect">Area where the coordinate system is due to be drawn.</param>
        public void DrawCoordinates(Graphics dc, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            PointF center = this.GetRectangleCenter(coordinateArea);
            for (int rowIndex = 0; rowIndex <= this.rowCount + 2; ++rowIndex)
            {
                float Y = coordinateBorder.Y + coordinateBorder.Height * (float)rowIndex / (float)(this.rowCount + 2);
                dc.DrawLine(this.coordinatePen, coordinateBorder.X, Y, coordinateBorder.X + coordinateBorder.Width, Y);
            }
            for (int colIndex = 0; colIndex <= this.colCount + 2; ++colIndex)
            {
                float X = coordinateBorder.X + coordinateBorder.Width * (float)colIndex / (float)(this.colCount + 2);
                dc.DrawLine(this.coordinatePen, X, coordinateBorder.Y, X, coordinateBorder.Y + coordinateBorder.Height);
            }
            dc.DrawLine(this.axisPen, coordinateBorder.X, center.Y, coordinateBorder.X + coordinateBorder.Width, center.Y);
            dc.DrawLine(this.axisPen, center.X, coordinateBorder.Y, center.X, coordinateBorder.Y + coordinateBorder.Height);
            dc.DrawRectangle(this.axisPen, coordinateArea.X, coordinateArea.Y, coordinateArea.Width, coordinateArea.Height);
        }

        /// <summary>
        /// Draws the image to the specified area.
        /// </summary>
        /// <param name="dc">Graphical device context.</param>
        /// <param name="image">Image.</param>
        /// <param name="rect">Area where the image is due to be drawn.</param>
        public void DrawImage(Graphics dc, Image image, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            RectangleF imageRectangle = this.AlignInside(image.Size, coordinateArea);
            dc.DrawImage(image, imageRectangle);
        }

        /// <summary>
        /// Draws the 2D graph to the specified area with the possibly highlighted cursor node and graph selection.
        /// </summary>
        /// <param name="dc">Graphical device context.</param>
        /// <param name="graph">2D graph.</param>
        /// <param name="cursor">Node at the actual cursor.</param>
        /// <param name="selection">Actual selection of the nodes of the graph.</param>
        /// <param name="rect">Area where the graph is due to be drawn.</param>
        public void DrawGraph(Graphics dc, Graph graph, Graph.Node cursor, Graph.Selection selection, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);

            for (int row = 0; row < graph.Rows; ++row)
            {
                for (int col = 0; col < graph.Columns; ++col)
                {
                    if (graph[row, col] != null)
                    {
                        PointF point1 = this.GetNodeCenter(row, col, coordinateBorder);
                        foreach (Graph.Edge edge in graph[row, col].Edges)
                        {
                            Graph.Node node = edge.ToNode;
                            PointF point2 = this.GetNodeCenter(node.Row, node.Col, coordinateBorder);
                            dc.DrawLine(this.nodePen, point1, point2);
                        }
                    }
                }
            }

            for (int row = 0; row < graph.Rows; ++row)
            {
                for (int col = 0; col < graph.Columns; ++col)
                {
                    if (graph[row, col] != null)
                    {
                        RectangleF nodeRectangle = this.GetNodeRectangle(row, col, coordinateBorder);
                        Brush brush = Brushes.Transparent;
                        Brush selectedBrush = Brushes.Transparent;
                        Pen pen = this.nodePen;
                        if (selection != null && selection.IsValid)
                        {
                            if (selection.NodeDepths[row, col] == 0)
                            {
                                pen = this.selectedNodePen;
                                selectedBrush = this.selectedNodeBrush;
                            }
                            else if (selection.NodeDepths[row, col] > 0)
                            {
                                pen = this.selectedContextPen;
                                selectedBrush = this.selectedContextBrush;
                            }
                        }
                        switch (graph[row, col].Color)
                        {
                            case -1: brush = Brushes.LightPink; break;
                            case 0: brush = Brushes.White; break;
                            case 1: brush = Brushes.Black; break;
                        }
                        if (graph[row, col] == cursor)
                        {
                            dc.FillRectangle(brush, nodeRectangle);
                            dc.FillRectangle(selectedBrush, nodeRectangle);
                            dc.DrawRectangle(pen, nodeRectangle.X, nodeRectangle.Y, nodeRectangle.Width, nodeRectangle.Height);
                        }
                        else
                        {
                            dc.FillEllipse(brush, nodeRectangle);
                            dc.FillEllipse(selectedBrush, nodeRectangle);
                            dc.DrawEllipse(pen, nodeRectangle);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Draws the selection box to the specified area.
        /// </summary>
        /// <param name="dc">Graphical device context.</param>
        /// <param name="selectionBox">Selection box rectangle.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        public void DrawSelectionBox(Graphics dc, RectangleF selectionBox, Rectangle rect)
        {
            Rectangle drawRectangle = this.GetCursorRectangle(selectionBox, rect);
            dc.DrawRectangle(this.selectionPen, drawRectangle);
        }

        /// <summary>
        /// Returns the rectangle over the image corresponding to the given node.
        /// </summary>
        /// <param name="row">Row.</param>
        /// <param name="col">Column.</param>
        /// <param name="image">Image.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Rectangle over the image corresponding to the given node.</returns>
        public RectangleF GetImageClipRectangle(int row, int col, Image image, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            RectangleF imageRectangle = this.AlignInside(image.Size, coordinateArea);
            float width = coordinateBorder.Width / (float)(this.colCount + 2);
            float height = coordinateBorder.Height / (float)(this.rowCount + 2);
            float x0 = coordinateBorder.X + col * width;
            float y0 = coordinateBorder.Y + row * height;
            float x1 = x0 + width;
            float y1 = y0 + height;
            float ix = image.Size.Width * (x0 - imageRectangle.X) / imageRectangle.Width;
            float iy = image.Size.Height * (y0 - imageRectangle.Y) / imageRectangle.Height;
            float iwidth = image.Size.Width * (x1 - x0) / imageRectangle.Width;
            float iheight = image.Size.Height * (y1 - y0) / imageRectangle.Height;
            return new RectangleF(ix, iy, iwidth, iheight);
        }

        private PointF GetNodeCenter(int row, int col, RectangleF coordinateBorder)
        {
            float width = coordinateBorder.Width / (float)(this.colCount + 2);
            float height = coordinateBorder.Height / (float)(this.rowCount + 2);
            float x = coordinateBorder.X + col * width + width / 2.0f;
            float y = coordinateBorder.Y + row * height + height / 2.0f;
            return new PointF(x, y);
        }

        private RectangleF GetNodeRectangle(int row, int col, RectangleF coordinateBorder)
        {
            float width = coordinateBorder.Width / (float)(this.colCount + 2);
            float height = coordinateBorder.Height / (float)(this.rowCount + 2);
            float x = coordinateBorder.X + col * width + width / 4.0f;
            float y = coordinateBorder.Y + row * height + height / 4.0f;
            return new RectangleF(x, y, width / 2.0f, height / 2.0f);
        }

        /// <summary>
        /// Returns the coordinate position of the cursorPosition with respect to  
        /// the coordinate system placed in the given specified area.
        /// </summary>
        /// <param name="cursorPosition">Original position of the cursor in pixels in the given specified area.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Coordinates of the cursor with respect to the coordinate system.</returns>
        public PointF GetCoordinates(Point cursorPosition, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            PointF center = this.GetRectangleCenter(coordinateArea);
            float unitInPixels = this.GetUnitInPixels(coordinateArea);
            return new PointF(
                ((float)cursorPosition.X - center.X) / unitInPixels,
                ((float)center.Y - cursorPosition.Y) / unitInPixels);
        }

        /// <summary>
        /// Returns the coordinate size of the cursorSize with respect to 
        /// the coordinate system placed in the given specified area.
        /// </summary>
        /// <param name="cursorSize">Original size in pixels in the given specified area.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Size with respect to the coordinate system.</returns>
        public SizeF GetCoordinateSize(Size cursorSize, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            float unitInPixels = this.GetUnitInPixels(coordinateArea);
            return new SizeF(
                (float)cursorSize.Width / unitInPixels,
                (float)cursorSize.Height / unitInPixels);
        }

        /// <summary>
        /// Returns the coordinate rectangle of the cursorRectangle with respect to 
        /// the coordinate system placed in the given specified area.
        /// </summary>
        /// <param name="cursorRectangle">Original rectangle in pixels in the given specified area.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Rectangle with respect to the coordinate system.</returns>
        public RectangleF GetCoordinateRectangle(Rectangle cursorRectangle, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            PointF center = this.GetRectangleCenter(coordinateArea);
            float unitInPixels = this.GetUnitInPixels(coordinateArea);
            PointF location = new PointF(
                ((float)cursorRectangle.X - center.X) / unitInPixels,
                ((float)center.Y - cursorRectangle.Y) / unitInPixels);
            SizeF size = new SizeF(
                (float)cursorRectangle.Width / unitInPixels,
                (float)cursorRectangle.Height / unitInPixels);
            return new RectangleF(location, size);
        }

        /// <summary>
        /// Returns the position of the cursor (in pixels) corresponding to the coordinateCursor
        /// in the cooridnate system placed in the given specified area.
        /// </summary>
        /// <param name="coordinateCursor">Position of the cursor in the coordinate system.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Position of the cursor (in pixels).</returns>
        public Point GetCursor(PointF coordinateCursor, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            PointF center = this.GetRectangleCenter(coordinateArea);
            float unitInPixels = this.GetUnitInPixels(coordinateArea);
            return new Point(
                (int)(unitInPixels * coordinateCursor.X + center.X),
                (int)(center.Y - unitInPixels * coordinateCursor.Y));
        }

        /// <summary>
        /// Returns the size (in pixels) corresponding to the coordinateSize
        /// in the cooridnate system placed in the given specified area.
        /// </summary>
        /// <param name="coordinateSize">Size placed in the coordinate system.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Size (in pixels).</returns>
        public Size GetCursorSize(SizeF coordinateSize, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            float unitInPixels = this.GetUnitInPixels(coordinateArea);
            return new Size(
                (int)(unitInPixels * coordinateSize.Width),
                (int)(unitInPixels * coordinateSize.Height));
        }

        /// <summary>
        /// Returns the rectangle (in pixels) corresponding to the coordinateRectangle
        /// in the cooridnate system placed in the given specified area.
        /// </summary>
        /// <param name="coordinateRectangle">Rectangle placed in the coordinate system.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Rectangle (in pixels).</returns>
        public Rectangle GetCursorRectangle(RectangleF coordinateRectangle, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            RectangleF coordinateArea = this.GetCoordinateArea(coordinateBorder);
            PointF center = this.GetRectangleCenter(coordinateArea);
            float unitInPixels = this.GetUnitInPixels(coordinateArea);
            Point location = new Point(
                (int)(unitInPixels * coordinateRectangle.X + center.X),
                (int)(center.Y - unitInPixels * coordinateRectangle.Y));
            Size size = new Size(
                (int)(unitInPixels * coordinateRectangle.Width),
                (int)(unitInPixels * coordinateRectangle.Height));
            return new Rectangle(location, size);
        }

        /// <summary>
        /// Returns the node of the 2D graph at the given cursor position (or null).
        /// </summary>
        /// <param name="graph">2D graph.</param>
        /// <param name="cursor">Position of the cursor in the given specified area.</param>
        /// <param name="rect">Area where the coordinate system and the graph is placed.</param>
        /// <returns>Node of the 2D graph at the given cursor position (or null).</returns>
        public Graph.Node GetNodeAtCursor(Graph graph, Point cursor, Rectangle rect)
        {
            if (graph == null)
            {
                return null;
            }
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            float width = coordinateBorder.Width / (float)(this.colCount + 2);
            float height = coordinateBorder.Height / (float)(this.rowCount + 2);
            if (coordinateBorder.Contains(cursor))
            {
                float x = (float)cursor.X - coordinateBorder.X;
                float y = (float)cursor.Y - coordinateBorder.Y;
                int col = (int)(x / width);
                int row = (int)(y / height);
                RectangleF nodeRectangle = this.GetNodeRectangle(row, col, coordinateBorder);
                if (nodeRectangle.Contains(cursor))
                {
                    return graph[row, col];
                }
            }
            return null;
        }

        /// <summary>
        /// Returns the invalidation rectangle corresponding to the specified [row, col] position.
        /// </summary>
        /// <param name="row">Row.</param>
        /// <param name="col">Column.</param>
        /// <param name="rect">Area where the coordinate system is placed.</param>
        /// <returns>Invalidation rectangle corresponding to the specified [row, col] position.</returns>
        public Rectangle GetInvalidateRectangle(int row, int col, Rectangle rect)
        {
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            float width = coordinateBorder.Width / (float)(this.colCount + 2);
            float height = coordinateBorder.Height / (float)(this.rowCount + 2);
            float x = coordinateBorder.X + col * width;
            float y = coordinateBorder.Y + row * height;
            return new Rectangle((int)x, (int)y, (int)width + 1, (int)height + 1);
        }

        /// <summary>
        /// Returns the selection of the 2D graph inside the given specified selection area.
        /// </summary>
        /// <param name="graph">2D graph.</param>
        /// <param name="selectionArea">Area of the selection.</param>
        /// <param name="maxDepth">Maximal context depth.</param>
        /// <param name="maxDistance">Maximal context distance.</param>
        /// <param name="rect">Area where the coordinate system and the graph is placed.</param>
        /// <returns>Selection of the 2D graph inside the given specified selection area.</returns>
        public Graph.Selection GetSelectionInArea(Graph graph, Rectangle selectionArea, int maxDepth, int maxDistance, Rectangle rect)
        {
            if (graph == null)
            {
                return null;
            }
            RectangleF coordinateBorder = this.GetCoordinateBorder(rect);
            float width = coordinateBorder.Width / (float)(this.colCount + 2);
            float height = coordinateBorder.Height / (float)(this.rowCount + 2);
            int topRow = 0;
            int leftCol = 0;
            int bottomRow = this.rowCount + 1;
            int rightCol = this.colCount + 1;
            if (selectionArea.Y >= coordinateBorder.Y + height / 2.0f)
            {
                topRow = (int)((selectionArea.Y - coordinateBorder.Y + height / 2.0f) / height);
            }
            if (selectionArea.X >= coordinateBorder.X + width / 2.0f)
            {
                leftCol = (int)((selectionArea.X - coordinateBorder.X + width / 2.0f) / width);
            }
            if (selectionArea.Y + selectionArea.Height <= coordinateBorder.Y + coordinateBorder.Height - height / 2.0f)
            {
                bottomRow = (int)((selectionArea.Y + selectionArea.Height - coordinateBorder.Y - height / 2.0f) / height);
            }
            if (selectionArea.X + selectionArea.Width <= coordinateBorder.X + coordinateBorder.Width - width / 2.0f)
            {
                rightCol = (int)((selectionArea.X + selectionArea.Width - coordinateBorder.X - width / 2.0f) / width);
            }
            if (topRow <= bottomRow && leftCol <= rightCol)
            {
                return new Graph.Selection(graph, topRow, leftCol, bottomRow, rightCol, maxDepth, maxDistance);
            }
            else return null;
        }

        private RectangleF AlignInside(SizeF size, RectangleF rect)
        {
            float sizeRatio = size.Width / size.Height;
            float rectRatio = rect.Width / rect.Height;
            if (Math.Abs(sizeRatio - rectRatio) < 1.0e-6)
            {
                return rect;
            }
            else if (sizeRatio < rectRatio)
            {
                float width = sizeRatio * rect.Height;
                float offsetX = (rect.Width - width) / 2.0f;
                return new RectangleF(rect.X + offsetX, rect.Y, width, rect.Height);
            }
            else
            {
                float height = rect.Width / sizeRatio;
                float offsetY = (rect.Height - height) / 2.0f;
                return new RectangleF(rect.X, rect.Y + offsetY, rect.Width, height);
            }
        }

        private PointF GetRectangleCenter(RectangleF rect)
        {
            return new PointF(rect.X + rect.Width / 2.0f, rect.Y + rect.Height / 2.0f);
        }
        
        private RectangleF GetInnerRectangle(float marginWidth, float marginHeight, RectangleF rect)
        {
            return new RectangleF(rect.X + marginWidth, rect.Y + marginHeight,
                rect.Width - 2.0f * marginWidth, rect.Height - 2.0f * marginHeight);
        }

        private RectangleF GetCoordinateBorder(RectangleF rect)
        {
            float marginSize = Math.Min(rect.Width, rect.Height) * this.margin;
            RectangleF marginRect = this.GetInnerRectangle(marginSize, marginSize, rect);
            return this.AlignInside(new SizeF(this.colCount + 2, this.rowCount + 2), marginRect);
        }

        private RectangleF GetCoordinateArea(RectangleF coordinateBorder)
        {
            float marginWidth = (coordinateBorder.Width / (float)(this.colCount + 2));
            float marginHeight = (coordinateBorder.Height / (float)(this.rowCount + 2));
            return this.GetInnerRectangle(marginWidth, marginHeight, coordinateBorder);
        }

        private float GetUnitInPixels(RectangleF coordinateArea)
        {
            return Math.Max(coordinateArea.Width, coordinateArea.Height) / 2.0f;
        }

        private int rowCount = 10;
        private int colCount = 10;

        private readonly float margin = 0.05f;
        private readonly Pen coordinatePen = new Pen(Color.Gray, 1.0f);
        private readonly Pen axisPen = new Pen(Color.DarkGray, 2.0f);
        private readonly Pen nodePen = new Pen(Color.Black, 1.0f);
        private readonly Pen selectedNodePen = new Pen(Color.Orange, 4.0f);
        private readonly Pen selectedContextPen = new Pen(Color.Green, 4.0f);
        private readonly Brush selectedNodeBrush = new SolidBrush(Color.FromArgb(127, Color.Orange));
        private readonly Brush selectedContextBrush = new SolidBrush(Color.FromArgb(127, Color.Green));
        private readonly Pen selectionPen = new Pen(Color.Black, 2.0f);
    }
}
